﻿using System;
using System.Reflection;
using System.Reflection.Emit;
using System.Threading;

namespace PGMapper
{
    class TrackedCreator
    {
        // global module builder for the "TrackedTypes" in-memory assembly
        static AssemblyBuilder assemblyBuilder;
        static ModuleBuilder moduleBuilder;

        static TrackedCreator()
        {
            AppDomain thisDomain = Thread.GetDomain();
            assemblyBuilder = thisDomain.DefineDynamicAssembly(
                new AssemblyName { Name = "TrackedTypes" }, AssemblyBuilderAccess.Run);

            moduleBuilder = assemblyBuilder.DefineDynamicModule(assemblyBuilder.GetName().Name);
        }

        /// <summary>
        /// Creates a type that subclasses T and implements ITrackChanges. If T already implements
        /// ITrackChanges, it will just be returned without generating any new types.
        /// </summary>
        public static Type CreateTracked<T>(MappedType<T> mappedTable)
        {
            ILGenerator il;
            Type baseType = typeof(T);

            if (typeof(ITrackChanges).IsAssignableFrom(baseType))
                return baseType; // you crazy bastard, you implemented ITrackChanges yourself

            // Allow non-public constructors in the base type
            ConstructorInfo baseConstructor = baseType.GetConstructor(
                BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public, null,
                Type.EmptyTypes, new ParameterModifier[0]);

            if (baseConstructor == null)
                throw new Exception("Could not locate a protected parameterless constructor for the type " + baseType);

            // Get the method of ChangeTracker we want to call
            MethodInfo changedMethod = typeof(ChangeTracker).GetMethod("PropertyChanged");

            // This is all from inspecting an actual compiled desired subclass using Reflector.
            // Also from: http://www.codeproject.com/KB/dotnet/Creating_Dynamic_Types.aspx

            // Create a type that subclasses T and implements ITrackChanges
            TypeBuilder builder = moduleBuilder.DefineType("Tracked" + baseType.Name,
                TypeAttributes.Class | TypeAttributes.Public | TypeAttributes.AutoClass |
                TypeAttributes.AnsiClass | TypeAttributes.BeforeFieldInit | TypeAttributes.AutoLayout,
                baseType, new Type[] { typeof(ITrackChanges) });

            // We're implementing ITrackChanges, so we'll need a field for ChangeTracker
            FieldBuilder trackerField = builder.DefineField("tracker", 
                typeof(ChangeTracker), FieldAttributes.Private);

            // Find the Tracker getter supplied by ITrackChanges and override (implement) it
            MethodInfo baseGetTracker = typeof(ITrackChanges).GetProperty("Tracker").GetGetMethod();

            MethodBuilder getTracker = builder.DefineMethod(baseGetTracker.Name,
                MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.SpecialName | 
                MethodAttributes.NewSlot | MethodAttributes.Virtual | MethodAttributes.Final,
                baseGetTracker.ReturnType, Type.EmptyTypes);

            il = getTracker.GetILGenerator();
            il.DeclareLocal(typeof(ChangeTracker));
            il.Emit(OpCodes.Ldarg_0);
            il.Emit(OpCodes.Ldfld, trackerField);
            il.Emit(OpCodes.Ret);

            // override the setter for each method marked with [Column]
            foreach (MappedProperty mappedColumn in mappedTable.Columns)
            {
                // override just the setter method
                MethodInfo baseSetter = mappedColumn.Property.GetSetMethod(true);

                if (!baseSetter.IsVirtual)
                    throw new Exception(string.Format("The set method of {0}.{1} must be virtual (unless you implement ITrackChanges yourself).", 
                        baseSetter.DeclaringType, mappedColumn.Property.Name));

                // mimic the access level of the method we're overriding
                MethodAttributes access = MethodAttributes.Public;
                if (baseSetter.IsPrivate) access = MethodAttributes.Private;
                if (baseSetter.IsFamily) access = MethodAttributes.Family;

                MethodBuilder setColumn = builder.DefineMethod(baseSetter.Name,
                    access | MethodAttributes.HideBySig |
                    MethodAttributes.SpecialName | MethodAttributes.Virtual, 
                    baseSetter.ReturnType, new Type[] { baseSetter.GetParameters()[0].ParameterType });

                il = setColumn.GetILGenerator();
                il.Emit(OpCodes.Ldarg_0);
                il.Emit(OpCodes.Ldarg_1);
                il.Emit(OpCodes.Call, baseSetter);
                il.Emit(OpCodes.Ldarg_0);
                il.Emit(OpCodes.Ldfld, trackerField);
                il.Emit(OpCodes.Ldstr, mappedColumn.Property.Name);
                il.Emit(OpCodes.Callvirt, changedMethod);
                il.Emit(OpCodes.Ret);
            }

            // Create a public no-args constructor
            ConstructorBuilder constructor = builder.DefineConstructor(
                MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.SpecialName | 
                MethodAttributes.RTSpecialName, CallingConventions.Standard, Type.EmptyTypes);

            il = constructor.GetILGenerator();
            il.Emit(OpCodes.Ldarg_0);
            il.Emit(OpCodes.Call, baseConstructor);
            il.Emit(OpCodes.Ldarg_0);
            il.Emit(OpCodes.Newobj, typeof(ChangeTracker).GetConstructor(Type.EmptyTypes));
            il.Emit(OpCodes.Stfld, trackerField);
            il.Emit(OpCodes.Ret);

            // bake and serve.
            return builder.CreateType();
        }
    }
}
